assert(PandaDKPMaster, "PandaDKPMaster not found!")


local kSyncStateUnknown = 0
local kSyncStateInProgress = 1
local kSyncStateInSync = 2

------------------------------
--      Are you local?      --
------------------------------
local PandaDKPMaster = PandaDKPMaster
local Sync =
{
   SyncTimer = nil,
   SyncState = kSyncStateUnknown,      -- Unknown if no sync has been done, in progess if we're talking to someone, in sync if we've confirmed with everyone online
   Others = {},
   -- ["player"] = 
   --    {
   --       State = kSyncStateInProgress,
   --       LastMessageTime = time(),
   --       Checksums =
   --          {
   --             DB =
   --             {
   --                ALL = md5,
   --                yyyymmddhhmmss = md5,
   --                etc...
   --             },
   --             ITEM =
   --             {
   --                ALL = md5,
   --                yyyymmddhhmmss = md5,
   --                etc...
   --             },
   --             etc...
   --          },
          
   --    },
   Unknown = {},                       -- A list of checksums that others have that we don't.
}
PandaDKPMaster.Sync = Sync


function Sync:SendSyncRequest()
   Sync.SyncTimer = nil
   local check = Sync:GetChecksum('DB', 'ALL')
   PandaDKPMaster.Comm:Send("GUILD", string.format("SYNC DB ALL %s", check))
end


function Sync:ReceivedSync(sender, type, datestamp, checksum)
   -- Verify the type and datestamp
   if (type ~= 'DB' and type ~= 'ITEM' and type ~= 'ALIAS' and type ~= 'DKP' and type ~= 'EVENT') then return end
   if ( not (datestamp == 'ALL' or tonumber(datestamp) >= tonumber(PandaDKPMaster:ThirtyDaysAgo())) ) then return end
   
   -- See if we need to recalculate our summary
   PandaDKPMaster.Events:SummarizeEvents(PandaDKPMaster:GetDatestamp(PandaDKPMaster:ThirtyDaysAgo()))
   
   -- If the checksum matches, send back an OK message
   local check = Sync:GetChecksum(type, datestamp)
   if (check == checksum) then
      PandaDKPMaster.Comm:Send("WHISPER", string.format("SYNCOK %s %s", type, datestamp), sender)
      return
   end
   
   -- Else, whisper the sender our checksum value
   local message = string.format("DIFF %s %s %s", type, datestamp, check)
   PandaDKPMaster.Comm:Send("WHISPER", message, sender)
end


function Sync:ReceivedSyncOk(sender, type, datestamp)
   if (type == 'DB' and datestamp == 'ALL') then
      -- Woot!  All's good!
      PandaDKPMaster.db.realm.LastSync = time()
      return
   end
   
end


function Sync:ReceivedDiff(sender, type, datestamp, checksum)
   
   -- Save the player's value
   
   -- Drill down with a new sync command if necessary
   if ('DB' == type) then
      local message = string.format("SYNC %s %s %s", 'ITEM', 'ALL', Sync:CalculateItemChecksum())
      PandaDKPMaster.Comm:Send("WHISPER", message, sender)
      message = string.format("SYNC %s %s %s", 'ALIAS', 'ALL', Sync:CalculateAliasChecksum())
      PandaDKPMaster.Comm:Send("WHISPER", message, sender)
      message = string.format("SYNC %s %s %s", 'DKP', 'ALL', Sync:CalculateDkpChecksum())
      PandaDKPMaster.Comm:Send("WHISPER", message, sender)
      message = string.format("SYNC %s %s %s", 'EVENT', 'ALL', Sync:CalculateEventChecksum('ALL'))
      PandaDKPMaster.Comm:Send("WHISPER", message, sender)
      return
   elseif ('EVENT' == type and 'ALL' == datestamp) then
      -- Send one sync message for every day
      local thirtyDays = tonumber(PandaDKPMaster:GetDatestamp(PandaDKPMaster:ThirtyDaysAgo()))
      local ds = PandaDKPMaster:GetDatestamp(PandaDKPMaster:GetTimestamp())
      while (tonumber(ds) >= thirtyDays) do
         local message = string.format("SYNC %s %s %s", 'EVENT', ds, Sync:CalculateEventChecksum(ds))
         PandaDKPMaster.Comm:Send("WHISPER", message, sender)
         ds = PandaDKPMaster:GetDatestamp(PandaDKPMaster:TimestampAdd(ds, -1 * 60 * 60))
      end
      return
   end
   
   -- otherwise, send a CHK command for the appropriate table
   if ('ALL' ~= datestamp) then Sync:SendCheckCommand(datestamp, sender) end
   
   -- If we're refreshing, also ask for the other tables to be sent as well
   if (Sync.Refreshing) then
      Sync.Refreshing = nil
      PandaDKPMaster.Comm:Send("WHISPER", "PUB ITEM", sender)
      PandaDKPMaster.Comm:Send("WHISPER", "PUB ALIAS", sender)
      PandaDKPMaster.Comm:Send("WHISPER", "PUB DKP", sender)
   end
   
   -- After some time has elapsed, send another sync command
   if (nil == Sync.SyncTimer) then
      Sync.SyncTimer = PandaDKPMaster:ScheduleTimer(function() PandaDKPMaster.Sync:SendSyncRequest() end, 15 * 60)
   end
end


-- Checks my list of events on the specified date. For events I know about and they
-- don't, send the event. For events they know about and I don't, send UNK.
function Sync:ReceivedChecklist(sender, datestamp, checksums)
   if ('ALL' == datestamp) then return end
   
   -- Get a list of events on that date
   local function DateFilter(event, arg)
      if (PandaDKPMaster:GetDatestamp(event.ts) == arg) then
         return true
      end
      return false
   end
   local matches = PandaDKPMaster.Events:FindEvents(DateFilter, PandaDKPMaster:GetDatestamp(datestamp))
   
   -- Turn both tables into hash tables instead of arrays
   checksums = PandaDKPMaster:ArrayToHash(checksums)
   matches = PandaDKPMaster:ArrayToHash(matches)
   
   -- Construct a list of events we both have
   local toRemove = {}
   for md5, _ in pairs(checksums) do
      if (nil ~= matches[md5]) then toRemove[md5] = true end
   end
   
   -- Remove those events from both lists
   for md5, _ in pairs(toRemove) do
      checksums[md5] = nil
      matches[md5] = nil
   end
   
   -- Events left in the 'checksums' list are events we don't know about
   for md5, _ in pairs(checksums) do
      PandaDKPMaster.Comm:Send("WHISPER", string.format('UNK %s', md5), sender)
   end
   
   -- Events left in the 'matches' list are events they don't know about
   for md5, _ in pairs(matches) do
      PandaDKPMaster.Events:SendEvent(md5)
   end
end


-- Gets the checksum from the database, calculating it if necessary
function Sync:GetChecksum(type, datestamp)
   if (type ~= 'EVENT') then datestamp = 'ALL' end
   
   -- Calculate the checksum if necessary
   if (nil == PandaDKPMaster.db.realm.DKP.Checksums[type]) then
      PandaDKPMaster.db.realm.DKP.Checksums[type] = {}
   end
   if (nil == PandaDKPMaster.db.realm.DKP.Checksums[type][datestamp]) then
      PandaDKPMaster.db.realm.DKP.Checksums[type][datestamp] = Sync:CalculateChecksum(type, datestamp)
   end
   
   return PandaDKPMaster.db.realm.DKP.Checksums[type][datestamp]
end


function Sync:ResetChecksum(type, timestamp)
   local datestamp = PandaDKPMaster:GetDatestamp(timestamp)
   if (nil ~= PandaDKPMaster.db.realm.DKP.Checksums[type]) then
      if (type == 'EVENT') then PandaDKPMaster.db.realm.DKP.Checksums[type][datestamp] = nil end
      PandaDKPMaster.db.realm.DKP.Checksums[type]['ALL'] = nil
   end
   PandaDKPMaster.db.realm.DKP.Checksums['DB']['ALL'] = nil
end


function Sync:CalculateChecksum(type, datestamp)
   if ('DB' == type) then
      return Sync:CalculateDbChecksum()
   elseif ('ITEM' == type) then
      return Sync:CalculateItemChecksum()
   elseif ('ALIAS' == type) then
      return Sync:CalculateAliasChecksum()
   elseif ('DKP' == type) then
      return Sync:CalculateDkpChecksum()
   elseif ('EVENT' == type) then
      return Sync:CalculateEventChecksum(datestamp)
   end
end


function Sync:CalculateDbChecksum()
   local checks = {}
   tinsert(checks, Sync:GetChecksum('ITEM', 'ALL'))
   tinsert(checks, Sync:GetChecksum('ALIAS', 'ALL'))
   tinsert(checks, Sync:GetChecksum('DKP', 'ALL'))
   tinsert(checks, Sync:GetChecksum('EVENT', 'ALL'))
   
   return PandaDKPMaster.MD5Lib:MD5(table.concat(checks, ''))
end


function Sync:CalculateItemChecksum()
   -- Sort items by item ID
   local ids = {}
   for itemId, record in pairs(PandaDKPMaster.db.realm.DKP.Items) do tinsert(ids, itemId) end
   sort(ids)
   
   -- Make a long string with all IDs, dkp and pool
   local checks = {}
   for i, itemId in ipairs(ids) do
      local record = PandaDKPMaster.db.realm.DKP.Items[itemId]
      tinsert(checks, string.format("%s%.2f%s", record.itemId, record.dkp, record.pool))
   end
   
   return PandaDKPMaster.MD5Lib:MD5(table.concat(checks, ''))
end


function Sync:CalculateAliasChecksum()
   return PandaDKPMaster.MD5Lib:MD5('')
end


function Sync:CalculateDkpChecksum()
   local checks = {}
   for player, pools in pairs(PandaDKPMaster.db.realm.DKP.HistoricalDKP) do
      for pool, dkp in pairs(pools) do
         tinsert(checks, string.format("%s%s%.2f", player, pool, dkp))
      end
   end
   sort(checks)
   
   return PandaDKPMaster.MD5Lib:MD5(table.concat(checks, ''))
end

function Sync:CalculateEventChecksum(datestamp)
   local matches = nil
   
   local function DateFilter(event, arg)
      if (PandaDKPMaster:GetDatestamp(event.ts) == arg) then
         return true
      end
      return false
   end
   
   if ('ALL' == datestamp) then
      matches = PandaDKPMaster.Events:FindEvents(function() return true end, nil)
   else
      matches = PandaDKPMaster.Events:FindEvents(DateFilter, datestamp)
   end
   sort(matches)
   
   return PandaDKPMaster.MD5Lib:MD5(table.concat(matches, ''))
end


function Sync:SendCheckCommand(datestamp, recipient)
   -- Find all events that match the date
   local function DateFilter(event, arg)
      if (PandaDKPMaster:GetDatestamp(event.ts) == arg) then
         return true
      end
      return false
   end
   local matches = PandaDKPMaster.Events:FindEvents(DateFilter, datestamp)
   
   local message = string.format('CHK %s {%s}', datestamp, table.concat(matches, ', '))
   PandaDKPMaster.Comm:Send("WHISPER", message, recipient)
end


function Sync:RefreshDatabase()
   -- Clear out the DB
   PandaDKPMaster.db.realm.DKP =
   {
      Checksums = {},
      Pools = {},
      Items = {},
      Players = {},
      HistoricalDKP = {},
      Aliases = {},
      Events = {},
      Logging = {},
   }
   
   -- Tell the other functions we're refreshing and then run it.
   Sync.AcceptingItem = true
   Sync.AcceptingAlias = true
   Sync.AcceptingDkp = true
   Sync.Refreshing = true
   Sync:SendSyncRequest()
end


function Sync:HandlePubCommand(sender, table)
   if ("ITEM" == table) then
      Sync:PublishItemTable(sender)
   elseif ("ALIAS" == table) then
      Sync:PublishAliasTable(sender)
   elseif ("DKP" == table) then
      Sync:PublishDkpTable(sender)
   end
end


function Sync:HandlePubDoneCommand(sender, table)
   if ("ITEM" == table) then
      Sync.AcceptingItem = false
   elseif ("ALIAS" == table) then
      Sync.AcceptingAlias = false
   elseif ("DKP" == table) then
      Sync.AcceptingDkp = false
      PandaDKPMaster:RecalculateAllDKP()
   end
end


function Sync:PublishItemTable(recipient)
   for itemId, data in pairs(PandaDKPMaster.db.realm.DKP.Items) do
      local message = string.format('ITEM %s %.2f "%s" "%s" "%s" "%s"', itemId, data.dkp,
       data.pool, data.type, data.subtype, data.name)
      PandaDKPMaster.Comm:Send("WHISPER", message, recipient)
   end
   PandaDKPMaster.Comm:Send("WHISPER", "PUBDONE ITEM", recipient)
end

function Sync:PublishAliasTable(recipient)
   PandaDKPMaster.Comm:Send("WHISPER", "PUBDONE ALIAS", recipient)
end


function Sync:PublishDkpTable(recipient)
   for player, pools in pairs(PandaDKPMaster.db.realm.DKP.HistoricalDKP) do
      for pool, dkp in pairs(pools) do
         local message = string.format('DKP %s "%s" %.2f', player, pool, dkp)
         PandaDKPMaster.Comm:Send("WHISPER", message, recipient)
      end
   end
   PandaDKPMaster.Comm:Send("WHISPER", "PUBDONE DKP", recipient)
end

function Sync:OnItemMessage(itemId, dkp, pool, itemtype, subtype, name)
   if (Sync.AcceptingItem) then
      PandaDKPMaster.Items:SetItemValue(itemId, dkp, pool, itemtype, subtype, name)
      Sync:ResetChecksum("ITEM", nil)
   end
end

function Sync:OnAliasMessage(player, alias)
   if (Sync.AcceptingAlias) then
   end
end

function Sync:OnDkpMessage(player, pool, dkp)
   if (Sync.AcceptingDkp) then
      if (nil == PandaDKPMaster.db.realm.DKP.HistoricalDKP[player]) then PandaDKPMaster.db.realm.DKP.HistoricalDKP[player] = {} end
      PandaDKPMaster.db.realm.DKP.HistoricalDKP[player][pool] = dkp
      Sync:ResetChecksum("DKP", nil)
   end
end